﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Reflection;

namespace Summerset.Isbndb
{
    /// <summary>
    /// Represents the base type for paginated responses from any of the IBSNdb.com collections.
    /// </summary>
    /// <typeparam name="ElementType">The type of element this page will contain.  This is determined based on the collection that will generate this response.</typeparam>
    public abstract class ResponsePageBase<ElementType> where ElementType : ResponseElementBase
    {
        #region Constrctor + Methods

        /// <summary>
        /// Initializes a new instance of the BooksResponsePage class by invoking a request to the ISBNdb BooksCollection 
        /// endpoint.  
        /// Once the data inside of this class is loaded by this constructor, the data is immutable.
        /// </summary>
        /// <param name="pageNumber">The page number that will be returned by the response.  ISBNdb returns at most 10 results per page.  The page number is a 1-based index.</param>        
        /// <param name="requestUri">The Uri that this request will be executed against.  This URI must contain the collection base uri and any additional arguments, including the page number that will be requested.</param>
        /// <exception cref="System.Exception">
        /// A generic exception will be thrown if the Api receives the request, but returns a node named ErrorMessage.  
        /// The InnerText from that node will be returned in the Exception's Message property.
        /// </exception>
        /// <remarks>
        /// In addition to the generic Exception that may be thrown if the returned <see cref="XmlDocument"/> contains an ErrorMessage element,
        /// this constructor may also throw any of the exceptions thrown by the <see cref="XmlDocument"/> Load(string) method.
        /// </remarks>
        public ResponsePageBase(int pageNumber, Uri requestUri)
        {

            // Attempt to load a new XmlDocument object from the provided Uri            
            _xml.Load(requestUri.AbsoluteUri);


            // If there is a problem with our page request, the xml data will contain an <ErrorMessage> element that we can examine for our exception data
            if (_xml.SelectNodes(ErrorXpath).Count > 0)
            {
                throw new Exception(_xml.SelectSingleNode(ErrorXpath).InnerText);
            }

        }

        #endregion
        #region Consts

        const string PageNumAttrib = "page_number";
        const string ShownResultsAttrib = "shown_results";
        const string MaxResultsAttrib = "total_results";
        const string ServerTimeAttrib = "server_time";

        const string KeystatsXpath = "//KeyStats";
        const string ErrorXpath = "//ErrorMessage";

        #endregion
        #region Properties + Backing Fields

        // main backing field
        readonly XmlDocument _xml = new XmlDocument();

        /// <summary>
        /// Gets the page number.  
        /// This should always equal the PageNumber argument passed in this object's constructor,
        /// but we'll read the page number out of the retrieved XML document anyway.
        /// </summary>
        public int PageNumber
        {
            get { return int.Parse(_xml.SelectSingleNode(ListElementXpath).Attributes[PageNumAttrib].Value); }
        }

        /// <summary>
        /// Gets the number of results that are present in this page.  
        /// This will always be between 0 and 10.
        /// </summary>
        public int ShownResults
        {
            get { return int.Parse(_xml.SelectSingleNode(ListElementXpath).Attributes[ShownResultsAttrib].Value); }
        }

        /// <summary>
        /// Gets a value that indicates the total number of Books that were found matching the 
        /// provided arguments.  
        /// </summary>
        public int MaximumResults
        {
            get { return int.Parse(_xml.SelectSingleNode(ListElementXpath).Attributes[MaxResultsAttrib].Value); }
        }

        /// <summary>
        /// Gets the server response time.
        /// </summary>
        /// <remarks>
        /// Unlike the other attributes, this attribute is on the document node, not the *list node
        /// </remarks>
        public DateTimeOffset ServerResponseTime
        {
            get { return DateTimeOffset.Parse(_xml.DocumentElement.Attributes[ServerTimeAttrib].Value); }
        }

        /// <summary>
        /// Gets the <see cref="Keystats"/> for this response. Keystats are information about how
        /// many requests have been executed against the ISBNdb.com API using your API key.
        /// </summary>
        public Keystats Keystats
        {
            get
            {
                if (_ks == null)
                {
                    var keystatnode = _xml.SelectSingleNode(KeystatsXpath);
                    _ks = new Keystats(keystatnode);                    
                }

                return _ks;
            }
        }
        private Keystats _ks;

        /// <summary>
        /// Gets the elements that have been returned as part of this response page.
        /// </summary>
        public ElementType[] Items
        {
            get
            {
                if (_items == null)
                {
                    var elementNodes = _xml.SelectNodes(ElementXpath);

                    _items = new ElementType[elementNodes.Count];

                    for (int i = 0; i < elementNodes.Count; i++)
                    {
                        // use reflection to find the appropriate consturctor 
                        // we know all elements use a constructor that takes a single xml node argument
                        ConstructorInfo ctorInfo = typeof(ElementType).GetConstructor(new Type[] { typeof(XmlElement) });

                        // invoke the constructor
                        _items[i] = (ElementType)ctorInfo.Invoke(new object[]{(XmlElement)elementNodes[i]});
                    }
                }

                return _items;
            }
        }
        ElementType[] _items;

        #endregion
        #region Abstract Properties

        /// <summary>
        /// Gets the xpath expression that will be used to retrieve the list element.  There will only be one ListElement per page of results.
        /// </summary>
        protected abstract string ListElementXpath { get; }

        /// <summary>
        /// Gets the xpath expression that can be used to retrieve the individual elements.  Each page of results will contain 0-10 elements.
        /// </summary>
        protected abstract string ElementXpath { get; }

        #endregion
    }
}
